#!/usr/bin/env python3
# Copyright 2020 Efabless Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import odb

import os
import sys
import click
from subprocess import Popen, PIPE

from reader import OdbReader


def invoke_padring(config_file_name, output_file_name, lefs):
    print("Invoking padring to generate a padframe")
    padring_command = []
    padring_command.append("padring")
    for lef in lefs:
        padring_command.extend(["-L", lef])
    padring_command.extend(["--def", output_file_name])
    padring_command.append(config_file_name)

    p = Popen(padring_command, stdout=PIPE, stdin=PIPE, stderr=PIPE, encoding="utf8")

    output = p.communicate()
    print("STDERR:")
    print("\n".join(output[1].splitlines()[-10:]))
    print("STDOUT:")
    print(output[0].strip())

    print("Padring exit code:", p.returncode)
    assert p.returncode == 0, p.returncode
    assert os.path.exists(output_file_name)


def chunker(seq, size):
    chunks = [seq[i::size] for i in range(size)]
    # sort by type
    chunks.sort(key=lambda pad_pair: pad_pair[1])
    return chunks


def diff_lists(l1, l2):
    return list(list(set(l1) - set(l2)) + list(set(l2) - set(l1)))


def generate_cfg(north, east, south, west, corner_pads, width, height):
    cfg = []
    cfg.append(f"AREA {width} {height} ;")
    cfg.append("")

    assert len(corner_pads) == 4, corner_pads
    cfg.append(f"CORNER {corner_pads[0][0]} SW {corner_pads[0][1]} ;")
    cfg.append(f"CORNER {corner_pads[1][0]} NW {corner_pads[1][1]} ;")
    cfg.append(f"CORNER {corner_pads[2][0]} NE {corner_pads[2][1]} ;")
    cfg.append(f"CORNER {corner_pads[3][0]} SE {corner_pads[3][1]} ;")

    cfg.append("")

    for pad in north:
        cfg.append(f"PAD {pad[0]} N {pad[1]} ;")

    cfg.append("")

    for pad in east:
        cfg.append(f"PAD {pad[0]} E {pad[1]} ;")

    cfg.append("")

    for pad in south:
        cfg.append(f"PAD {pad[0]} S {pad[1]} ;")

    cfg.append("")

    for pad in west:
        cfg.append(f"PAD {pad[0]} W {pad[1]} ;")

    return "\n".join(cfg)


@click.command()
@click.option(
    "--output",
    required=True,
    help="Output ODB file",
)
@click.option(
    "--output-def",
    help="Output DEF file",
)
@click.option(
    "-v",
    "--verilog-netlist",
    default=None,
    help="A verilog netlist containing pads and other user macros",
)
@click.option(
    "-d",
    "--def-netlist",
    default=None,
    help="A DEF netlist containing unplaced pads and other user macros",
)
@click.option(
    "-l",
    "--input-lef",
    multiple=True,
    required=True,
    help="LEF file needed to have a proper view of the DEF files",
)
@click.option(
    "--odb-lef",
    required=True,
    help="LEF file to be included in output odb",
)
@click.option("-w", "--width", help="Width of the die area.")
@click.option("-h", "--height", help="Height of the die area.")
@click.option(
    "-c",
    "--padframe-config",
    required=True,
    help="Configuration file: input to padring",
)
@click.option(
    "-P",
    "--pad-name-prefixes",
    multiple=True,
    help="Padname prefixes",
)
@click.option(
    "-i",
    "--init-only",
    default=False,
    is_flag=True,
    help="Only generate a configuration file to be user edited.",
)
@click.option(
    "-C",
    "--working-dir",
    default=".",
    help="Working directory to create temporary files needed.",
)
@click.option(
    "-s", "--special-nets", multiple=True, help="Net names to mark as special"
)
@click.argument("design")
def padringer(
    output,
    output_def,
    verilog_netlist,
    def_netlist,
    input_lef,
    odb_lef,
    width,
    height,
    padframe_config,
    pad_name_prefixes,
    init_only,
    working_dir,
    special_nets,
    design,
):
    """
    Reads in a structural verilog containing pads and a LEF file that
    contains at least those pads and produces a DEF file with the padframe.
    TODO:
    core placement
    config init,
    external config
    """

    config_file_name = padframe_config
    lefs = input_lef

    working_def = f"{working_dir}/{design}.pf.def"
    working_cfg = f"{working_dir}/{design}.pf.cfg"

    for lef in lefs:
        assert os.path.exists(lef), lef + " doesn't exist"

    # hard requirement of a user netlist either as a DEF or verilog
    # this is to ensure that the padframe will contain all pads in the design
    # whether the config is autogenerated or user-provided
    assert (
        verilog_netlist is not None or def_netlist is not None
    ), "One of --verilog_netlist or --def-netlist is required"

    # Step 1: create an openDB database from the verilog/def using OpenSTA's read_verilog
    if verilog_netlist is not None:
        assert (
            def_netlist is None
        ), "Only one of --verilog_netlist or --def-netlist is required"
        assert design is not None, "--design is required"

        openroad_script = []
        for lef in lefs:
            openroad_script.append(f"read_lef {lef}")
        openroad_script.append(f"read_verilog {verilog_netlist}")
        openroad_script.append(f"link_design {design}")
        openroad_script.append(f"write_def {working_def}")
        openroad_script.append("exit")

        p = Popen(["openroad"], stdout=PIPE, stdin=PIPE, stderr=PIPE, encoding="utf8")

        openroad_script = "\n".join(openroad_script)

        output = p.communicate(openroad_script)
        print("STDOUT:")
        print(output[0].strip())
        print("STDERR:")
        print(output[1].strip())
        print("openroad exit code:", p.returncode)
        assert p.returncode == 0, p.returncode
        # TODO: check for errors
    else:
        assert def_netlist is not None
        working_def = def_netlist

    assert os.path.exists(working_def), "DEF file doesn't exist"

    top = OdbReader(odb_lef, working_def)

    print(f"Top-level design name: {top.name}")

    ## Step 2: create a simple data structure with pads from the library
    # types: corner, power, other
    pads = {}
    libs = top.db.getLibs()
    for lib in libs:
        masters = lib.getMasters()
        for m in masters:
            name = m.getName()
            if m.isPad():
                assert any(name.startswith(p) for p in pad_name_prefixes), name
                print("Found pad:", name)
                pad_type = m.getType()
                pads[name] = pad_type
                if pad_type == "PAD_SPACER":
                    print("Found PAD_SPACER:", name)
                elif pad_type == "PAD_AREAIO":
                    # using this for special bus fillers...
                    print("Found PAD_AREAIO", name)
            if m.isEndCap():
                # FIXME: regular endcaps
                assert any(name.startswith(p) for p in pad_name_prefixes), name
                assert not m.isPad(), name + " is both pad and endcap?"
                print("Found corner pad:", name)
                pads[name] = "corner"

    print()
    print("The I/O library contains", len(pads), "cells")
    print()

    assert len(pads) != 0, "^"

    ## Step 3: Go over instances in the design and extract the used pads
    used_pads = []
    used_corner_pads = []
    other_instances = []
    for inst in top.block.getInsts():
        inst_name = inst.getName()
        master_name = inst.getMaster().getName()
        if inst.isPad():
            assert any(
                master_name.startswith(p) for p in pad_name_prefixes
            ), master_name
            print("Found pad instance", inst_name, "of type", master_name)
            used_pads.append((inst_name, master_name))
        elif inst.isEndCap():
            # FIXME: regular endcaps
            assert any(
                master_name.startswith(p) for p in pad_name_prefixes
            ), master_name
            print("Found pad instance", inst_name, "of type", master_name)
            print("Found corner pad instance", inst_name, "of type", master_name)
            used_corner_pads.append((inst_name, master_name))
        else:
            assert not any(
                master_name.startswith(p) for p in pad_name_prefixes
            ), master_name
            other_instances.append(inst_name)

    # FIXME: if used_corner_pads aren't supposed to be instantiated
    assert len(used_corner_pads) == 4, used_corner_pads

    print()
    print(
        "The user design contains",
        len(used_pads),
        "pads, 4 corner pads, and",
        len(other_instances),
        "other instances",
    )
    print()
    assert len(used_pads) != 0, "^"

    ## Step 4: Generate a CFG or verify a user-provided config

    if config_file_name is not None:
        assert os.path.exists(config_file_name), config_file_name + " doesn't exist"
        with open(config_file_name, "r") as f:
            lines = f.readlines()
        user_config_pads = []
        for line in lines:
            if line.startswith("CORNER") or line.startswith("PAD"):
                tokens = line.split()
                assert len(tokens) == 5, tokens
                inst_name, master_name = tokens[1], tokens[3]
                if (
                    not pads[master_name] == "PAD_SPACER"
                    and not pads[master_name] == "PAD_AREAIO"
                ):
                    user_config_pads.append((inst_name, master_name))
            elif line.startswith("AREA"):
                tokens = line.split()
                assert len(tokens) == 4, tokens
                width = int(tokens[1])
                height = int(tokens[2])

        assert sorted(user_config_pads) == sorted(used_pads + used_corner_pads), (
            "Mismatch between the provided config and the provided netlist. Diff:",
            diff_lists(user_config_pads, used_pads + used_corner_pads),
        )

        print("User config verified")
        working_cfg = config_file_name
    else:
        # TODO: get minimum width/height so that --width and --height aren't required
        assert width is not None, "--width is required"
        assert height is not None, "--height is required"

        # auto generate a configuration

        # TODO: after calssification, center power pads on each side
        north, east, south, west = chunker(used_pads, 4)

        with open(working_cfg, "w") as f:
            f.write(
                generate_cfg(north, east, south, west, used_corner_pads, width, height)
            )

    if not init_only:
        invoke_padring(working_cfg, working_def, lefs)
    else:
        print(
            "Padframe config generated at",
            working_cfg,
            f"Modify it and re-run this program with the '-cfg {working_cfg}' option",
        )
        sys.exit()

    print("Applying pad placements to the design DEF")

    padframe = OdbReader(lefs, working_def)

    assert padframe.name == "PADRING", padframe.name

    print("Padframe design name:", padframe.name)

    # Mark special nets
    if special_nets is not None:
        for net in top.block.getNets():
            net_name = net.getName()
            if net_name in special_nets:
                print("Marking", net_name, "as a special net")
                net.setSpecial()
                for iterm in net.getITerms():
                    iterm.setSpecial()

    # get minimum width/height (core-bounded)

    placed_cells_count = 0
    created_cells_count = 0
    for inst in padframe.block.getInsts():
        assert inst.isPad() or inst.isEndCap(), (
            inst.getName() + " is neither a pad nor corner pad"
        )

        inst_name = inst.getName()
        master = inst.getMaster()
        master_name = master.getName()
        x, y = inst.getLocation()
        orient = inst.getOrient()

        if (inst_name, master_name) in used_pads + used_corner_pads:
            original_inst = top.block.findInst(inst_name)
            assert original_inst is not None, "Failed to find " + inst_name
            assert original_inst.getPlacementStatus() == "NONE", (
                inst_name + " is already placed"
            )
            original_inst.setOrient(orient)
            original_inst.setLocation(x, y)
            original_inst.setPlacementStatus("FIRM")
            placed_cells_count += 1
        else:
            # must be a filler cell
            new_inst = odb.dbInst_create(
                top.block, top.db.findMaster(master_name), inst_name
            )
            assert new_inst is not None, "Failed to create " + inst_name
            new_inst.setOrient(orient)
            new_inst.setLocation(x, y)
            new_inst.setPlacementStatus("FIRM")
            created_cells_count += 1

    if output_def:
        odb.write_def(top.block, output_def)
    odb.write_db(top.db, output)
    print("Done.")


if __name__ == "__main__":
    padringer()
